// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;

namespace SnapObjects.Data
{
    /// <summary>
    ///     Represents an object that can package multiple types of elements together.
    /// </summary>
    public class DataPacker : IDataPacker
    {
        private const string SIMPLEROOT = "DataPacker";

        private readonly IDictionary<string, DataKey> _replaces;
        private readonly IDictionary<string, IDataPackerFormater> _datas;

        /// <summary>
        /// Initializes a new instance of the DataPacker class.
        /// </summary>
        public DataPacker() : this(DataFormat.Json)
        {

        }

        /// <summary>
        ///     Initializes a new instance of the DataPacker class.
        /// </summary>
        /// <param name="dataFormat">
        ///     An enumerate value of the DataFormat enumeration.
        ///     It only supports DataFormat.Json so far.
        /// </param>
        public DataPacker(DataFormat dataFormat) : this(String.Empty, dataFormat)
        {

        }

        /// <summary>
        /// Initializes a new instance of the DataPacker class.
        /// </summary>
        /// <param name="raw">
        ///     A raw string which contains the data package.
        ///     The format of the data package must conform to the specified format.
        /// </param>
        /// <param name="dataFormat">
        ///     An enumerate value of the DataFormat enumeration.
        ///     It only supports DataFormat.Json so far.
        /// </param>
        public DataPacker(string raw, DataFormat dataFormat)
        {
            _replaces = new Dictionary<string, DataKey>(StringComparer.OrdinalIgnoreCase);
            _datas = new Dictionary<string, IDataPackerFormater>(StringComparer.OrdinalIgnoreCase);

            this.DataFormat = dataFormat;

            if (!String.IsNullOrEmpty(raw))
            {
                this.ParseRaw(raw);
            }
        }

        /// <summary>
        ///     Gets the number of elements that have been added into the IDataPacker object.
        /// </summary>
        public int Count => _datas.Count;

        /// <summary>
        ///     Gets the keys for the elements which have been added into the IDataPacker object.
        /// </summary>
        public string[] Keys => _datas.Keys.ToArray();

        /// <summary>
        ///     Gets the raw string of text which contains all of the elements in the IDataPacker object.
        /// </summary>
        public string Raw => this.GetTextString(this.DataFormat);

        /// <summary>
        ///     Gets the format of the text specified when creating the IDataPacker object.
        /// </summary>
        public DataFormat DataFormat { get; }

        /// <summary>
        ///     Add a raw text string as an element to the IDataPacker object.
        /// </summary>
        /// <param name="key">The key for the element, which is the identifier of the element in the IDataPacker object.</param>
        /// <param name="value">The raw text string, which is the value of the element to be added to the IDataPacker object.</param>
        public void AddRaw(string key, string value)
        {
            _datas.Add(key, new RawPackerFormater(value));
        }

        /// <summary>
        ///     Checks whether a specified element exists in the current IDataPacker object.
        /// </summary>
        /// <param name="key">The key of the element to look for.</param>
        /// <returns>Returns true if the element exists; otherwise, false.</returns>
        public bool ContainsKey(string key)
        {
            return _datas.ContainsKey(key);
        }

        /// <summary>
        ///     Removes the specified element from the IDataPacker object.
        /// </summary>
        /// <param name="key">The key of the element to remove.</param>
        /// <returns>Returns true if the element is successfully removed; returns false if the element was not found.</returns>
        public bool RemoveKey(string key)
        {
            if (_replaces.ContainsKey(key))
            {
                return _datas.Remove(key) && _replaces.Remove(key);
            }
            else
            {
                return _datas.Remove(key);
            }
        }

        internal void AddFormater(string key, IDataPackerFormater formater)
        {
            _datas.Add(key, formater);
        }

        internal void AddFormater(DataKey key, IDataPackerFormater formater)
        {
            var name = key.ToTextString();

            _datas.Add(name, formater);
            _replaces.Add(name, key);
        }

        /// <summary>
        ///     Gets a string in the specified format which represents all elements in the current IDataPacker object.
        /// </summary>
        /// <param name="format">
        ///     The format of the text string you want to get.
        ///     It supports DataFormat.Json and DataFormat.Xml so far.
        /// </param>
        /// <returns>Returns the string of text in the specified format.</returns>
        public string GetTextString(DataFormat format)
        {
            var writer = DataFormaterFactory.GetDataWriter(format);
            var formater = DataFormaterFactory.GetKeyFormater(format);

            var root = writer.WriteAsObject(SIMPLEROOT);

            foreach (var data in _datas)
            {
                var name = data.Key;

                if (_replaces.TryGetValue(name, out var key))
                {
                    name = formater.Format(key);
                }

                var value = data.Value.Format(format);

                root.WriteAsValue(name).WriteRawValue(value);
            }

            writer.WriteEndObject();

            return writer.ToString();
        }

        /// <summary>
        ///     Gets the raw string of text for the specified element.
        /// </summary>
        /// <param name="key">The key specifying which element to get the raw string of text.</param>
        /// <returns>Returns the raw string of text for the element if the key exists; otherwise, returns String.Empty.</returns>
        public string GetRaw(string key)
        {
            if (_datas.TryGetValue(key, out var v))
            {
                if (v is NormalPackerFormater<string>)
                {
                    return (string)v.Value;
                }

                var value = v.Format(this.DataFormat);

                return value;
            }

            return String.Empty;
        }

        private void ParseRaw(string raw)
        {
            var reader = DataFormaterFactory.GetDataReader(raw, this.DataFormat);

            if (reader.Read())
            {
                while (!reader.IsEnd)
                {
                    var value = reader.ReadAsRaw();

                    _datas.Add(value.Name, new RawPackerFormater(value.ToString()));
                }
            }
        }
    }
}
